/**
 * Copyright 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.api.adwords.starterkit.Synchronizer;

import com.google.api.adwords.starterkit.db.keys.TextAdKey;
import com.google.api.adwords.starterkit.db.structures.TextAdStructure;
import com.google.api.adwords.starterkit.db.tables.AdGroupTable;
import com.google.api.adwords.starterkit.db.tables.TextAdTable;
import com.google.api.adwords.starterkit.settings.StarterKitSettings;
import com.google.api.adwords.starterkit.utils.AdWordsReportHandler;
import com.google.api.adwords.starterkit.utils.DumpExceptions;
import com.google.api.adwords.starterkit.utils.RowUtils;
import com.google.api.adwords.starterkit.utils.ServiceUtils;
import com.google.api.adwords.starterkit.utils.Utils;
import com.google.api.adwords.v201101.cm.AdGroupAd;
import com.google.api.adwords.v201101.cm.AdGroupAdOperation;
import com.google.api.adwords.v201101.cm.AdGroupAdReturnValue;
import com.google.api.adwords.v201101.cm.AdGroupAdServiceInterface;
import com.google.api.adwords.v201101.cm.AdGroupAdStatus;
import com.google.api.adwords.v201101.cm.ApiError;
import com.google.api.adwords.v201101.cm.ApiException;
import com.google.api.adwords.v201101.cm.Operator;
import com.google.api.adwords.v201101.cm.Predicate;
import com.google.api.adwords.v201101.cm.PredicateOperator;
import com.google.api.adwords.v201101.cm.ReportDefinitionReportType;
import com.google.api.adwords.v201101.cm.TextAd;

import org.apache.log4j.Logger;

import java.rmi.RemoteException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * The Class Synchronizer is responsible to sync AdWords System with local db.
 * 
 * @author rodrigo.f.vale@gmail.com (Rodrigo Vale)
 */
public class TextAdSynchronizer {
  private static final Logger log = Logger.getLogger("StarterKit");
  private StarterKitSettings starterKitSetting;
  private ServiceUtils serviceUtils;
  public TextAdTable textAdTable;
  private AdGroupTable adGroupTable;

  /**
   * Instantiates a new synchronizer.
   * 
   * @param serviceUtils
   *          the service utils to access AdWords system
   * @param starterKitSetting
   *          the starterkit settings
   * @param conn
   *          the connection to local db
   */
  public TextAdSynchronizer(StarterKitSettings starterKitSetting, Connection conn) {
    this.setServiceUtils(new ServiceUtils(starterKitSetting));
    this.starterKitSetting = starterKitSetting;
    
    textAdTable = new TextAdTable(conn);
    adGroupTable = new AdGroupTable(conn);
  }

  /**
   * Process updates at textads table.
   */
  protected void put() {
    log.debug("Preparing TextAds to be sent to AdWords");
    int tries = 0;

    if (starterKitSetting.getTextAdSettings().getAutoPause()) {
      textAdTable.autoPause();
    }
    textAdTable.grantBanned();
    textAdTable.updateAdGroupsIds();

    while (tries < starterKitSetting.getRetriesOnError()) {
      HashSet<AdGroupAdOperation> operations 
          = new HashSet<AdGroupAdOperation>();
      log.info("Updating/Creating TextAds - Try: " + tries);
      try {
        ResultSet rs = textAdTable.getModified();

        log.debug("Processing Tuples" + tries);
        while (rs.next()) {
          TextAdStructure txtAdStructure = textAdTable.getStructure(rs);
          TextAd textAd = new TextAd();
          textAd.setHeadline(txtAdStructure.getHeadline());
          textAd.setDescription1(txtAdStructure.getDescription1());
          textAd.setDescription2(txtAdStructure.getDescription2());
          textAd.setDisplayUrl(txtAdStructure.getDisplayUrl());
          textAd.setUrl(txtAdStructure.getDestinationUrl());

          AdGroupAd adGroupAd = new AdGroupAd();
          adGroupAd.setAdGroupId(txtAdStructure.getAdGroupId());
          adGroupAd.setAd(textAd);
          adGroupAd.setStatus(txtAdStructure.getStatus());

          // Create operations.
          AdGroupAdOperation operation = new AdGroupAdOperation();
          operation.setOperand(adGroupAd);

          if ((txtAdStructure.getCreativeId() == 0)
              && (starterKitSetting.getTextAdSettings().getAutoCreation())) {
            operation.setOperator(Operator.ADD);
          } else {
            textAd.setId(txtAdStructure.getCreativeId());
            operation.setOperator(Operator.SET);
          }
          operations.add(operation);
        }
        rs.close();
      } catch (SQLException e3) {
        log.debug("SQL Error on saveTextAds. Error: " + e3.getMessage());
        log.debug("StarterKit will try to move on. Some updates may be lost.");
      }

      boolean error = processOperations(operations);

      // If no policy error we don't need to run the process again
      if (!error) {
        tries = starterKitSetting.getRetriesOnError();
      } else {
        Utils.sleep(starterKitSetting.getSleepOnPolicyError());
        tries++;
      }
    }
    textAdTable.resetStatus();
  }

  /**
   * Saves processed updates from TextAds to AdWords.
   * 
   * @param operations
   *          The updates to be sent to AdWords
   */
  protected boolean processOperations(HashSet<AdGroupAdOperation> operations) {
    boolean error = false;
    AdGroupAdServiceInterface service = getServiceUtils().getAdService();
    if (operations.size() > 0) {
      log.info("Processing: " + operations.size());
      List<HashSet<AdGroupAdOperation>> blocks = Utils.split(operations, 1000);
      for (HashSet<AdGroupAdOperation> opBlocks : blocks) {
        log.info("Processing Block: " + opBlocks.size());
        AdGroupAdOperation[] op = opBlocks
            .toArray(new AdGroupAdOperation[opBlocks.size()]);
        AdGroupAdReturnValue result = null;
        try {
          result = service.mutate(op);
          // Display ad group criteria.
          if (result != null && result.getValue() != null) {
            for (AdGroupAd adGroupAd : result.getValue()) {
              TextAd textAd = (TextAd) adGroupAd.getAd();
              long adGroupID = adGroupAd.getAdGroupId();
              int localAdGroupId = adGroupTable.getLocalId(adGroupID);
              TextAdKey textAdKey = new TextAdKey(localAdGroupId, textAd
                  .getHeadline(), textAd.getDescription1(), textAd
                  .getDescription2());
              textAdTable.setBatchId(textAdKey, textAd.getId(), false, false);
            }
            textAdTable.executeBatch();
          }
          log.debug("Updated " + op.length + " TextAds");
        } catch (ApiException e1) {
          error = true;
          log.info(e1.getErrors().length + " ApiExceptions. Removing it.");
          Pattern pattern = Pattern.compile("operations\\[(\\d+)\\].*");

          HashMap<String, Integer> errorsMsg = new HashMap<String, Integer>();
          HashMap<String, String> errorsTrigger = new HashMap<String, String>();
          int count = 0;
          for (ApiError apiError : e1.getErrors()) {
            int value = 0;
            String key = apiError.getErrorString();
            if (errorsMsg.containsKey(key)) {
              value = errorsMsg.get(key);
            }
            value++;
            errorsMsg.put(key, value);
            errorsTrigger.put(key, apiError.getTrigger());
            Matcher matcher = pattern.matcher(apiError.getFieldPath());
            if (matcher.matches()) {
              count++;
              int i = Integer.parseInt(matcher.group(1));
              AdGroupAd adGroupAd = op[i].getOperand();
              long adGroupID = adGroupAd.getAdGroupId();
              int localAdGroupId = adGroupTable.getLocalId(adGroupID);
              TextAd textAd = (TextAd) adGroupAd.getAd();
              String headline = textAd.getHeadline();
              String description1 = textAd.getDescription1();
              String description2 = textAd.getDescription2();
              TextAdKey textAdKey = new TextAdKey(localAdGroupId, headline,
                  description1, description2);
              textAdTable.setBatchUpdated(textAdKey, false, false, apiError
                  .getErrorString(), apiError.getTrigger());
            }
          }
          Iterator<String> i = errorsMsg.keySet().iterator();
          while (i.hasNext()) {
            String key = (String) i.next();
            String trigger = errorsTrigger.get(key);
            int value = errorsMsg.get(key);
            log.debug("Error: " + key + " [" + value + "] - Trigger Example: "
                + trigger);
            if (key.equals("InternalApiError."
                + "UNEXPECTED_INTERNAL_API_ERROR")) {
              Utils.sleep(starterKitSetting.getSleepOnInternalError());
            }
          }
          log.debug("Removed " + count + " ApiExceptions");
          textAdTable.executeBatch();
        } catch (RemoteException e) {
          error = true;
          DumpExceptions.dumpRemoteException(e);
        }
      }
    }
    return error;
  }

  /**
   * Process a specific creative from adwords.
   * 
   * @param row
   *          the AdWords report row representing the Creative
   * @param adGroupIdsMap
   *          the adgroup ids map
   * @throws SQLException
   *           the sQL exception
   */
  private void processReportData(HashMap<String, String> row,
      HashMap<Long, Integer> adGroupIdsMap) throws SQLException {
    String type = RowUtils.getString(row, "adType", "");

    if (type.equals("Text ad")) {
      TextAdStructure textAdStructure = new TextAdStructure();

      textAdStructure.setAdGroupId(RowUtils.getLong(row, "adGroupID", 0));
      textAdStructure.setCreativeId(RowUtils.getLong(row, "adID", -1));
      textAdStructure.setDescription1(RowUtils.getString(row,
          "descriptionLine1", ""));
      textAdStructure.setDescription2(RowUtils.getString(row,
          "descriptionLine2", ""));
      textAdStructure.setDestinationUrl(RowUtils.getString(row,
          "destinationURL", ""));
      textAdStructure.setDisplayUrl(RowUtils.getString(row, "displayURL", ""));
      textAdStructure.setHeadline(RowUtils.getString(row, "ad", ""));
      String status = RowUtils.getString(row, "adState", "").toUpperCase();
      textAdStructure.setStatus(AdGroupAdStatus.fromString(status));

      if (textAdStructure.getStatus() != AdGroupAdStatus.DISABLED) {
        if (adGroupIdsMap.containsKey(textAdStructure.getAdGroupId())) {

          textAdStructure.localAdGroupId = adGroupIdsMap.get(textAdStructure
              .getAdGroupId());

          TextAdKey textAdKey = new TextAdKey(textAdStructure.localAdGroupId,
              textAdStructure.getHeadline(), textAdStructure.getDescription1(),
              textAdStructure.getDescription2());

          TextAdStructure txtStructBD = textAdTable.get(textAdKey);

          if (txtStructBD == null) {
            textAdStructure.setStatusModifiable(false);
            textAdStructure.setBanned(false);
            textAdTable.addToBatch(textAdStructure, false, false);
          } else {
            textAdStructure.setLocalId(txtStructBD.getLocalId());
            textAdStructure
                .setStatusModifiable(txtStructBD.getCanModifyStatus());

            if (!textAdStructure.getStatus().equals(txtStructBD.getStatus())) {
              textAdStructure.setStatusModifiable(false);
            }

            textAdStructure.setBanned(false);
            textAdTable.addToBatch(textAdStructure, false, false);
          }
        }
      }
    }
  }

  /**
   * Load creatives from adwords.
   */
  public void get() {
    log.debug("Processing Creatives JSON Objects");
    HashMap<Long, Integer> adGroupIdsMap = adGroupTable.getAdGroupIdsMap();

    textAdTable.resetIsBanned(false);

    AdWordsReportHandler adWordsReportHandler = getReportHandler();

    try {
      int count = 0;
      while (adWordsReportHandler.hasNext()) {
        HashMap<String, String> row = adWordsReportHandler.next();
        processReportData(row, adGroupIdsMap);
        count++;
        if (count == 100000) {
          count = 0;
          textAdTable.executeBatch();
        }
      }
      textAdTable.executeBatch();
    } catch (SQLException e) {
      DumpExceptions.dumpSQLException(e);
    }
    
    textAdTable.resetStatus();
  }

  /**
   * Gets the creative structure report.
   * 
   * @return the creative structure report
   */
  public AdWordsReportHandler getReportHandler() {
    log.debug("Defining Creative Structure Report");

    String fields[] = new String[] { "Id", "AdGroupId", "Status",
        "CreativeDestinationUrl", "AdType", "Description1", "Description2",
        "PromotionLine", "DisplayUrl", "CampaignName", "AdGroupName" };

    String fileName = starterKitSetting.getCacheDir().concat(
        "CreativeStrututureReport.xml");

    String campaignStatus[] = null;
    if (starterKitSetting.getDownloadDeletedCampaigns()) {
      campaignStatus = new String[] { "ACTIVE", "PAUSED", "DELETED" };
    } else {
      campaignStatus = new String[] { "ACTIVE", "PAUSED" };
    }

    // Create campaing predicate.
    Predicate campaingPredicate = new Predicate();
    campaingPredicate.setField("CampaignStatus");
    campaingPredicate.setOperator(PredicateOperator.IN);
    campaingPredicate.setValues(campaignStatus);

    // Create TextAd predicate.
    Predicate textAdPredicate = new Predicate();
    textAdPredicate.setField("AdType");
    textAdPredicate.setOperator(PredicateOperator.EQUALS);
    textAdPredicate.setValues(new String[] { "TEXT_AD" });

    String status[] = new String[] { "ENABLED", "PAUSED" };    

    // Create AdGroup predicate.
    Predicate statusPredicate = new Predicate();
    statusPredicate.setField("Status");
    statusPredicate.setOperator(PredicateOperator.IN);
    statusPredicate.setValues(status);
           
    Predicate[] predicates = new Predicate[] { textAdPredicate,
        campaingPredicate, statusPredicate };

    AdWordsReportHandler adWordsReportHandler = new AdWordsReportHandler(
        getServiceUtils(), starterKitSetting);
    adWordsReportHandler.requestAdWordsReport(
        ReportDefinitionReportType.AD_PERFORMANCE_REPORT, fields, fileName,
        predicates);

    return adWordsReportHandler;
  }

  public void setServiceUtils(ServiceUtils serviceUtils) {
    this.serviceUtils = serviceUtils;
  }

  public ServiceUtils getServiceUtils() {
    return serviceUtils;
  }
}